/**
 * \file 3dx.cpp
  * 3D eXtended Module.
 * 
 * Currently supported:
 *	- loads vertices and triangular faces
 *  - per vertex normals
 *  - botched support for multiple set of texture coordinates
 * 
 * To do:
 *	- throw errors when file not found / error when parsing / binary x format...
 *	- quad faces - split it into two triangles
 *	- material support - sort of
 *  
 * Features:
 *	- provides a XMesh3D class, for loading .x files.
 *  - several test meshes of various complexity and features
 **
 *  Author: Mac Duy Hai, Sean Micklethwaite
 **
 * License... etc.
 */

#define	ENABLE_VERTEXNORMALS		true
#define	ENABLE_TEXTURE				true

#include <string>
#include <fstream>
#include "wtftypes.h"
#include "3d.h"
#include "3dx.h"
#include "glsl.h"

using namespace WtfGraphics;

/**** XParser ****/

void XParser::skipWs() {
	while(xfile().good() && isspace(xfile().peek())) {
		xfile().get();
	};
};

// returns true if found
bool XParser::locateBlock(std::vector<std::string>& vsTags, bool bPeek) {
	using namespace std;

	ifstream::pos_type pos = xfile().tellg();
	bool bFound = false;
	//
	// A block consists of a name followed by a '{' character.
	//

	vsTags.clear();

	while(xfile().good()) {
		skipWs();
		if(xfile().peek() == '{') {
			// Found the block - skip the character and return
			xfile().ignore(1);
			bFound = true;
			break;
		};

		vsTags.push_back(string());
		skipWs();
		xfile() >> vsTags[vsTags.size() - 1];

		if(vsTags[vsTags.size() - 1] == "}") {
			// End of block encountered
			break;
		};
	};

	// Return position to original if peeking
	if(bPeek) {
		xfile().seekg(pos);
	};

	return bFound;
};

void XParser::startBlock() {
	while(xfile().good()) {
		if(xfile().get() == '{') {
			return;
		};
	};

	throw std::runtime_error("XParser parse error - unexpected end of file - expected '{'");
};

void XParser::endBlock() {
	while(xfile().good()) {
		if(xfile().get() == '}') {
			return;
		};
	};

	throw std::runtime_error("XParser parse error - unexpected end of file - expected '}'");
};

void XParser::ReadMatrix(Transformation3D &t) {
	// DirectX matrices are row major, as are ours, but DirectX use's
	// row vectors, whereas we use column vectors, so transpose the
	// matrix afterwards.
	xfile() >> t[0][0]; xfile().ignore(1);
	xfile() >> t[0][1]; xfile().ignore(1);
	xfile() >> t[0][2]; xfile().ignore(1);
	xfile() >> t[0][3]; xfile().ignore(1);
	xfile() >> t[1][0]; xfile().ignore(1);
	xfile() >> t[1][1]; xfile().ignore(1);
	xfile() >> t[1][2]; xfile().ignore(1);
	xfile() >> t[1][3]; xfile().ignore(1);
	xfile() >> t[2][0]; xfile().ignore(1);
	xfile() >> t[2][1]; xfile().ignore(1);
	xfile() >> t[2][2]; xfile().ignore(1);
	xfile() >> t[2][3]; xfile().ignore(1);
	xfile() >> t[3][0]; xfile().ignore(1);
	xfile() >> t[3][1]; xfile().ignore(1);
	xfile() >> t[3][2]; xfile().ignore(1);
	xfile() >> t[3][3]; xfile().ignore(2, '\n');

	t = t.transpose();
};

void XParser::putBack(const std::string& str) {
	for(std::string::const_reverse_iterator i = str.rbegin(); i != str.rend(); i++) {
		xfile().putback(*i);
		if(xfile().fail()) {
			throw new std::runtime_error("XParser: putBack failed");
		};
	};
};

void XParser::putBack(const std::vector<std::string> &vsBlockTags) {
	std::string s;

	for(std::vector<std::string>::const_iterator i = vsBlockTags.begin(); i != vsBlockTags.end(); i++) {
		s += *i + " ";
	};

	putBack(s + "{");
};


/**** XMesh ****/

// Constructor
XMesh::XMesh(int features) : Mesh("XMesh") {
	useTangents = CHECKFLAG(features, FEATURE_TANGENTS);
}

XMesh::XMesh(const char *filename, int scale) : Mesh("XMesh") {
	Load(filename, Transformation3D::scale(scale));
}

// Destructor
XMesh::~XMesh() {
	
}

void XMesh::Read(std::ifstream *pXFile, const Transformation3D &trans) {
	using namespace std;

	// temporary variables
	vector<tVector>			verts;				// Vertex Buffer
	vector<tVector>			norms;				// Vertex Normals Buffer
	vector<fVector2D>		coords;				// Texture Coords Buffer
	vector<Triangle>		normals;			// Vertex Normals

	vector<iVector3D>		triIds;				// Triangle indices
	vector<iVector3D>		triNormIds;			// Triangle normal indices
	vector<TexCoordSet>		*coordSets = NULL;	// Texture coordinates sets
	TexCoordSet				coordSet;			// One texture coordinate set
	bool					useTexCoords = false;

	tScalar					fTmp;
	int						iNumVerts = 0;		// # of vertices
	int						i = 0, j = 0, m, n;	//	(counters)
	string					buf = "";
	vector<string>			vsBlockTags;

	// Set XFile pointer in XParser
	this->mpXFile = pXFile;

	while(locateBlock(vsBlockTags)) {

		// Switch on the block type (C++ can't actually "switch" on strings)
		if (vsBlockTags[0] == "Frame") {
			throw new runtime_error(WtfEngine::String("XMesh - unexpected Frame in ") + getName() + " - should this be an XModel?");
		} else if (vsBlockTags[0] == "FrameTransformMatrix") {
			// Ignore
			endBlock();
		} else if (vsBlockTags[0] == "Mesh") {

			// read in vertices
			xfile() >> n;
			xfile().ignore(1);

			// Preallocate
			verts.resize(iNumVerts + n);

			for (i = iNumVerts; i < (iNumVerts + n); i++) {
				xfile() >> verts[i][0]; xfile().ignore(1);		
				xfile() >> verts[i][1]; xfile().ignore(1);
				xfile() >> verts[i][2]; xfile().ignore(3,'\n');

				// Transform vertex by frame matrix
				verts[i] = trans * verts[i];

				// manually swap z & y. Negate z. <-- done by matrix
				//fTmp = verts[i][1];
				//verts[i][1] = -verts[i][2];
				//verts[i][2] = fTmp;
			}

			// read in faces - they all need to be triangles
			xfile() >> n;
			xfile().ignore(1);

			triIds.resize(n);

			for (i = 0; i < n; i++) {
				xfile() >> j;
				xfile().ignore(1);

				xfile() >> triIds[i][0]; xfile().ignore(1);
				xfile() >> triIds[i][1]; xfile().ignore(1);
				xfile() >> triIds[i][2]; xfile().ignore(5, '\n');

				// Add current vertex offset
				triIds[i][0] += iNumVerts;
				triIds[i][1] += iNumVerts;
				triIds[i][2] += iNumVerts;

				// create the triangle and push it into the mesh 
				addTriangle(Triangle(verts[triIds[i][0]],
									 verts[triIds[i][1]], 
									 verts[triIds[i][2]]));
			}

			// Stay inside the mesh block

		} else if (vsBlockTags[0] == "MeshNormals") {
			xfile() >> n;
			xfile().ignore(1);


			norms.resize(n);

			for (i = 0; i < n; i++) {
				xfile() >> norms[i][0] ; xfile().ignore(1);
				xfile() >> norms[i][1] ; xfile().ignore(1);
				xfile() >> norms[i][2] ; xfile().ignore(3,'\n');
				
				// Rotate the normal
				norms[i] = trans.getOrientation() * norms[i];
			}

			// normal triplets
			xfile() >> n;
			xfile().ignore(1);

			if(n != triIds.size()) {
				throw new std::runtime_error("XMesh parse error - # of mesh normal triplets does not match # of triangles");
			};
			triNormIds.resize(n);
			for (i = 0; i < n; i++) {
				xfile() >> j;
				xfile().ignore(1);

				xfile() >> triNormIds[i][0]; xfile().ignore(1);
				xfile() >> triNormIds[i][1]; xfile().ignore(1);
				xfile() >> triNormIds[i][2]; xfile().ignore(5, '\n');

				// Add current vertex offset
				triNormIds[i][0] += iNumVerts;
				triNormIds[i][1] += iNumVerts;
				triNormIds[i][2] += iNumVerts;

				// create the triangle and push it into the mesh 
				normals.push_back(Triangle(norms[triNormIds[i][0]],
									 norms[triNormIds[i][1]], 
									 norms[triNormIds[i][2]]));
			}

			// Does not contain other blocks, so leave the block
			endBlock();

		} else if (vsBlockTags[0] == "MeshTextureCoords") {
			// enable textures and set up variable, if this is first run
			if (!useTexCoords) {
				useTexCoords = true;
				coordSets = new std::vector<TexCoordSet>;			
			}

			xfile() >> n;
			xfile().ignore(1);

			coords.resize(n);
			for (i = 0; i < n; i++) {
				// this time, we need to use the triIds to get the order of vertices
				j = triIds[i / 3][i % 3];
				xfile() >> coords[j][0] ; xfile().ignore(1);
				xfile() >> coords[j][1] ; xfile().ignore(2,'\n');
				// make sure they y-tex coords are positive (they tend to be negative)
				if (coords[j][1] < 0) coords[j][1] += 1;
			}

			// now, push the tex coords into a texture coordinate set, remembering
			// to look up the triangle vertex IDs.
			coordSet.clear();
			for (i = 0; i < triIds.size(); i++) for(j = 0; j < 3; j++) {
				coordSet.push_back(coords[triIds[i][j]]);
			};

			// .. and push this set into the sets list
			coordSets->push_back(coordSet);

			// Leave the block
			endBlock();
		} else if(vsBlockTags[0] == "MeshMaterialList") {

			XMaterial::tRef rMaterial;

			//
			// This is where it gets complicated. Every triangle in the mesh can have a different
			// material applied to it. This is where (some form of) multitexturing comes into play.
			//

			xfile() >> m; // Number of materials;
			xfile().ignore(1);
			xfile() >> n; // Number of triangles;
			xfile().ignore(1);

			if(n != triIds.size()) {
				throw new std::runtime_error("XMesh parse error - # of material assignments does not match # triangles");
			};

			// TODO: support multiple materials
			// At the moment we ignore what we get from the file and just use
			// the first material.
			if(m != 1) {
				Log(LOG_WARNING, "XMesh - multiple materials not supported");
			};

			for(i = 0; i < n; i++) {
				xfile() >> j;
				xfile().ignore(1);
			};

			// Now the material itself. This can be a reference to an existing material, or
			// an inline material definition.
			locateBlock(vsBlockTags);
			if(vsBlockTags.size() == 0) {
				// No tags, so this is a reference. Get the name, load the material.
				string s;
				xfile() >> s;
				
				mrMaterial = WtfEngine::DataManager::GetInstance().Load<XMaterial>(WtfEngine::String(getName() + ":" + WtfEngine::String(s)));

				endBlock();

			} else if(vsBlockTags[0] == "Material") {
				// New material, don't export to data manager
				putBack(vsBlockTags);
				rMaterial = new XMaterial(getName());
				rMaterial->Read(this->mpXFile);
				mrMaterial = rMaterial;
			};

			// Exit block
			endBlock();
		};
	}; // End parser loop

	// close file, build VBO with enabled vertex normals + texture coords. Finally, clean up
	CreateVBO((normals.size() > 0) ? &normals : NULL, coordSets, useTangents);
	delete coordSets;
};


/* 
 *	Loads a .x file. Vertices are transformed by the supplied matrix.
 */
void XMesh::Load(const char *filename, const Transformation3D& trans) {
	std::vector<std::string>	vsBlockTags;
	Transformation3D			realTrans(Transformation3D::identity());

	// open file
	mpXFile = new std::ifstream(filename);


	// Skip to frames
	do {
		if(!locateBlock(vsBlockTags)) {
			throw std::runtime_error("XMesh: contains no frames");
		};
		if(vsBlockTags[0] == "FrameTransformMatrix") {
			Transformation3D t;
			ReadMatrix(t);
			realTrans = t.then(realTrans);
			endBlock();
			break;
		};
	} while(vsBlockTags[0] != "Frame");

	// Parse the file proper
	Read(mpXFile, realTrans.then(trans));

	delete mpXFile;
}

WtfEngine::IDataObject * XMesh::Load(const WtfEngine::StringPool::Entry &sName) {
	XMesh * pMesh = new XMesh(sName);
	pMesh->Load(((WtfEngine::String)sName).c_str(), Transformation3D::identity());
	return pMesh;
};


/**** XMaterial ****/

XMaterial::XMaterial(const WtfEngine::StringPool::Entry& sName)
: DiffuseTextureMaterial(sName) {
};

void XMaterial::Read(const WtfEngine::String &sFile) {
	using namespace std;
	vector<string> vsBlockTags;

	this->mpXFile = new ifstream(sFile.c_str());
	
	// Skip to material
	while(locateBlock(vsBlockTags)) {
		if(vsBlockTags[0] == "Material") {
			putBack(vsBlockTags);
			break;
		};
	};

	Read(this->mpXFile);

	delete this->mpXFile;
};

void XMaterial::Read(std::ifstream *pXFile) {
	using namespace std;
	vector<string> vsBlockTags;

	// Set input file
	this->mpXFile = pXFile;

	// Locate material block
	if(locateBlock(vsBlockTags) && vsBlockTags[0] == "Material") {
		
		//
		// Material format is:
		// fVector4D	: Face Colour
		// float		: Power
		// fVector3D	: Specular
		// fVector3D	: Emissive
		// [Optional TextureFilename]
		//

		xfile() >> mvDiffuse[0]; xfile().ignore(1); xfile() >> mvDiffuse[1]; xfile().ignore(1);
		xfile() >> mvDiffuse[2]; xfile().ignore(1); xfile() >> mvDiffuse[3]; xfile().ignore(2);
		xfile() >> mfPower; xfile().ignore(2, '\n');
		xfile() >> mvSpecular[0]; xfile().ignore(1); xfile() >> mvSpecular[1]; xfile().ignore(1); xfile() >> mvSpecular[2]; xfile().ignore(2, '\n');
		xfile() >> mvEmissive[0]; xfile().ignore(1); xfile() >> mvEmissive[1]; xfile().ignore(1); xfile() >> mvEmissive[2]; xfile().ignore(2, '\n');

		// Check for texture filename block
		if(locateBlock(vsBlockTags)) {
			if(vsBlockTags[0] == "TextureFilename") {
				string s;
				xfile() >> s;

				// Trim off the quotation marks and the semicolon
				s = s.substr(1, s.size() - 3);

				// Load the texture
				// XXX: assume that textures are stored at "media/textures/"
				WtfEngine::String sTexPath(WtfEngine::String("media/textures/") + s);
				if(ifstream(sTexPath.c_str()).good()) {
					mrTexture = WtfEngine::Texture::tCacheRef(sTexPath);
				} else {
					Log(LOG_ERROR, WtfEngine::String("Texture not found - ") + s);
				};
			} else {
				// Unknown block
			};

			// End block and material block
			endBlock();
			endBlock();
		} else {
			// Already left material block
		};
	} else {
		throw new runtime_error("XMaterial parse error - expected Material block");
	};
};

WtfEngine::IDataObject * XMaterial::Load(const WtfEngine::StringPool::Entry &sName) {
	XMaterial * pMat = new XMaterial(sName);
	pMat->Read(sName);
	return pMat;
};


/**** XModel ****/

XModel::XModel(const WtfEngine::StringPool::Entry &sName):
Model(sName) {
	
};


void XModel::Read(const WtfEngine::String& sFile) {
	using namespace std;

	vector<string>			vsBlockTags;

	// Open file
	this->mpXFile = new std::ifstream(sFile.c_str());

	// Initialize block stack
	mBlockStack.clear();
	mBlockStack.push_back(Transformation3D::identity());

	while(xfile().good()) {
		// Check for end of blocks
		if(!locateBlock(vsBlockTags, true)) {
			popBlock();
			if(mBlockStack.size() > 0) {
				endBlock();
			} else {
				break;
			};

		// Switch on the block type (C++ can't actually "switch" on strings)
		} else if (vsBlockTags[0] == "FrameTransformMatrix") {
			Transformation3D t;

			startBlock();
			ReadMatrix(t);
			endBlock();

			// Update block matrix
			getCurrentBlock().Matrix = t.then(getCurrentBlock().Matrix);
		} else if (vsBlockTags[0] == "Frame") {

			// Entering frame - push onto stack
			startBlock();
			pushBlock();

#		ifndef NDEBUG
			Log(LOG_DEBUG, WtfEngine::String("XMesh - loading frame ") + (vsBlockTags.size() > 1 ? vsBlockTags[1] : ""));
#		endif
		} else if (vsBlockTags[0] == "Mesh") {

			// Entering mesh - create new XMesh object
			// note - must use name name as the model, in order to share materials
			XMesh::tRef rMesh(new XMesh(getName()));

			rMesh->Read(this->mpXFile, getCurrentBlock().Matrix);
			if(rMesh->getMaterial().isNull()) {
				mvrMesh.push_back(rMesh);
			} else {
				mvrMesh.push_back(new MaterialBinding(vsBlockTags[1], rMesh->getMaterial(), rMesh));
			};
		} else if(vsBlockTags[0] == "Material") {
			
			// Load the material

			WtfEngine::StringPool::Entry sName = WtfEngine::String(WtfEngine::String(getName()) + ":" + vsBlockTags[1]);
			XMaterial::tRef rMaterial = new XMaterial(sName);

			rMaterial->Read(this->mpXFile);

			// Insert into data manager, so meshes can use the material
			WtfEngine::DataManager::GetInstance().Set<XMaterial>(&*rMaterial);
		} else {
			// Unhandled
			endBlock();
		};
	}; // End parser loop

	delete this->mpXFile;
};

void XModel::pushBlock() {
	mBlockStack.push_back(mBlockStack[mBlockStack.size() - 1]);
};

void XModel::popBlock() {
	mBlockStack.pop_back();
};

WtfEngine::IDataObject * XModel::Load(const WtfEngine::StringPool::Entry &sName) {
	XModel * pModel = new XModel(sName);
	pModel->Read(sName);
	return pModel;
};


/**** Tests ****/

using namespace Test;

/// Demonstrates a basic shader
Monkey::Monkey() : XMesh() {
	Load("media/meshes/monkey.x");
	// TEST: GLSL Shaders
	using namespace WtfEngine;
	GLSLShader* glslVS = new GLSLShader();
	GLSLShader* glslFS = new GLSLShader();
	GLSLProgram* glslProg = new ShaderPrograms::SpecularShader();
	glslVS->Load("src/shaders/specular.vert");
	glslFS->Load("src/shaders/specular.frag");
	glslProg->Load(glslVS, glslFS);
	
	//SetShader(glslProg);
}

/// Demonstrates a large mesh and sharp normals
Text::Text() : XMesh("media/meshes/barracks.x") {
	
}

/*
/// Demonstrates a large textured mesh
Cloister::Cloister() : XMesh() {
	//AllocateTextureSlots(1);
	//LoadTexture("media/textures/CloisterTexBaked.bmp",0,0);
	Load("media/meshes/cloister.x");	
}

/// Demonstrates multi texturing
Textured::Textured() : XMesh() {
	Load("media/meshes/crate.x");
	//AllocateTextureSlots(2);
	LoadTexture("media/textures/crate2.gif",0,0, GL_REPLACE);
	LoadTexture("media/textures/crate_lightmap.bmp",1,1);
}

/// Demonstrates Parallax Normal Mapping
Wall::Wall() : XMesh(FEATURE_TANGENTS) {
	Load("media/meshes/wall.x");
	
	// textures
	//AllocateTextureSlots(3);
	LoadTexture("media/textures/rockwall.png", 0, 0, GL_REPLACE);
	LoadTexture("media/textures/rockwall_normal.png", 1, 0);
	LoadTexture("media/textures/rockwall_height.png", 2, 0);
	
	// shader
	using namespace WtfEngine;
	GLSLShader* glslVS = new GLSLShader();
	GLSLShader* glslFS = new GLSLShader();
	GLSLProgram* glslProg = new ShaderPrograms::NormalShader();
	glslVS->Load("src/shaders/normal.vert");
	glslFS->Load("src/shaders/normal.frag");
	glslProg->Load(glslVS, glslFS);
	
	//SetShader(glslProg);
}

*/